//! Types for describing fields generically.

use stencil_tysys::{IntEncoding, ValueType};
use stencil_util::FieldName;

use crate::FieldType;

use super::ArrayShape;

/// Description of a accessor operation that can be performed on a field.
#[derive(Copy, Clone, Debug)]
pub enum FieldOp {
    Read(FieldReadOp),
    Write(FieldWriteOp),
}

/// Field reading operatinos.
#[derive(Copy, Clone, Debug)]
pub enum FieldReadOp {
    /// Gets the full value.
    Get,

    /// Gets a specific index's value.
    GetIndex,

    /// Gets the length of a dynamically sized container.
    GetLength,

    /// Gets a subfield within a cluster.
    GetSubfield,
}

/// Field writing operations.
#[derive(Copy, Clone, Debug)]
pub enum FieldWriteOp {
    /// Sets the full value.
    Set,

    /// Set's a specific index's value.
    SetIndex,

    /// Appends to the end of a dynamically sized container.
    Append,

    /// Sets a subfield within a cluster.
    SetSubfield,
}

/// Describes the typing shape of general type fields.
#[derive(Clone, Debug)]
pub struct FieldShape {
    /// The type of the unit.  This is the type used in the accessor signature.
    ///
    /// This is the base type we will update at a time.  Must have a statically
    /// known size.
    unit_ty: FieldType,

    /// The topology of the field, used to reason about the index args.
    topology: FieldTopology,
}

impl FieldShape {
    pub fn new(unit_ty: FieldType, topology: FieldTopology) -> Self {
        Self { unit_ty, topology }
    }

    /// Creates a new unit field shape.
    pub fn unit(ty: FieldType) -> Self {
        Self::new(ty, FieldTopology::Unit)
    }

    /// Creates a flat array field with a static size.
    pub fn flat_array(ty: FieldType, len: u64) -> Self {
        Self::new(ty, FieldTopology::Array(ArrayShape::flat(len)))
    }

    /// The "unit type" of the array structure.  This is the smallest size we assign to.
    pub fn unit_ty(&self) -> &FieldType {
        &self.unit_ty
    }

    /// Returns the unit type as a language value type.
    pub fn get_unit_value_ty(&self) -> ValueType {
        self.unit_ty().into()
    }

    /// Gets the full type of the field, including wrapping for as many layers
    /// of array as needed.
    pub fn get_full_ty(&self) -> ValueType {
        let mut ty = self.get_unit_value_ty();

        if let Some(shape) = self.topology.array_part() {
            for _len in shape.dims_iter() {
                // TODO convert to fixed size array when we support these types?
                ty = ValueType::list_of(ty);
            }
        }

        if self.topology.is_dynamic() {
            ty = ValueType::list_of(ty);
        }

        ty
    }

    pub fn topology(&self) -> &FieldTopology {
        &self.topology
    }
}

/// Describes the topology of a field.  This is used to reason about the index
/// arguments necessary on accessors.
#[derive(Clone, Debug)]
pub enum FieldTopology {
    /// A single unit field we can update all at once.
    Unit,

    /// A statically-bounded array shape that we can update single fields of.
    Array(ArrayShape),

    /// A sequence of units that we don't statically know the length of, but is
    /// available somehow.
    Sequence,

    /// A sequence of arrays which themselves have statically-known lengths, but
    /// the full sequence is of a dynamic length.
    ArraySequence(ArrayShape),
}

impl FieldTopology {
    /// Returns the number of index arguments needed.  These are needed in both
    /// getters and setters.
    pub fn num_index_args(&self) -> usize {
        match self {
            Self::Unit => 0,
            Self::Array(shape) => shape.dimensions(),
            Self::Sequence => 1,
            Self::ArraySequence(shape) => shape.dimensions() + 1,
        }
    }

    /// Returns the "array part" of the topology.
    pub fn array_part(&self) -> Option<&ArrayShape> {
        match self {
            Self::Array(shape) | Self::ArraySequence(shape) => Some(shape),
            _ => None,
        }
    }

    /// If the first index argument is a sequence, meaning it has dynamic bounds

    /// checking.  This arg would also conceptually have a stride of 0.
    pub fn is_dynamic(&self) -> bool {
        match self {
            Self::Sequence | Self::ArraySequence(_) => true,
            _ => false,
        }
    }
}

/// Named field entry within a buf type.
#[derive(Clone, Debug)]
pub struct FieldLayoutEntry {
    /// Name.
    ///
    /// This might be machine-generated.
    name: FieldName,

    /// The precise layout description.
    layout: FieldLayout,

    /// Operations we want to publicly expose.
    ops: Vec<FieldOp>,
}

impl FieldLayoutEntry {
    pub fn new(name: FieldName, layout: FieldLayout, ops: Vec<FieldOp>) -> Self {
        Self { name, layout, ops }
    }

    pub fn new_simple(name: FieldName, layout: FieldLayout) -> Self {
        Self::new(name, layout, Vec::new())
    }

    pub fn add_op(&mut self, op: FieldOp) {
        self.ops.push(op);
    }

    pub fn name(&self) -> &FieldName {
        &self.name
    }

    pub fn layout(&self) -> &FieldLayout {
        &self.layout
    }

    pub fn ops(&self) -> &[FieldOp] {
        &self.ops
    }
}

#[derive(Clone, Debug)]
pub enum FieldLayout {
    Unit(UnitFieldLayout),
    Seq(SequenceFieldLayout),
}

impl FieldLayout {
    pub fn loc(&self) -> &FieldLocator {
        match self {
            FieldLayout::Unit(l) => l.loc(),
            FieldLayout::Seq(l) => l.loc(),
        }
    }
}

/// Description of a concrete field.
///
/// This has a simple structure and does not require indexes.
#[derive(Clone, Debug)]
pub struct UnitFieldLayout {
    /// The type of the unit field.
    ty: FieldType,

    /// How to compute the location of the field.
    loc: FieldLocator,
}

impl UnitFieldLayout {
    pub fn new(ty: FieldType, loc: FieldLocator) -> Self {
        Self { ty, loc }
    }

    pub fn ty(&self) -> &FieldType {
        &self.ty
    }

    pub fn loc(&self) -> &FieldLocator {
        &self.loc
    }
}

/// Description of a generic indexable field structure.
///
/// These have a complex structure, but permits granular access with indexes.
#[derive(Clone, Debug)]
pub struct SequenceFieldLayout {
    /// The units within the field structure.
    ///
    /// These themselves look like `UnitFieldLayout`s but are part of this
    /// larger structure.
    unit_ty: FieldType,

    /// How to compute the location of the field.
    loc: FieldLocator,

    /// The shape of the array that we index into.
    ///
    /// This might be wrapped with one more dimension of variable length as
    /// indicated by the `len` field.
    ///
    /// This should not be a unit shape, as that would make more sense as a
    /// `UnitFieldLayout`.
    shape: ArrayShape,

    /// Description of the sequence length.
    len: SequenceLenDesc,
}

impl SequenceFieldLayout {
    pub fn new(
        unit_ty: FieldType,
        loc: FieldLocator,
        shape: ArrayShape,
        len: SequenceLenDesc,
    ) -> Self {
        Self {
            unit_ty,
            loc,
            shape,
            len,
        }
    }

    pub fn unit_ty(&self) -> &FieldType {
        &self.unit_ty
    }

    pub fn loc(&self) -> &FieldLocator {
        &self.loc
    }

    pub fn shape(&self) -> &ArrayShape {
        &self.shape
    }

    pub fn len(&self) -> &SequenceLenDesc {
        &self.len
    }

    /// Checks if this sequence has a variable length.
    pub fn is_variable_size(&self) -> bool {
        self.len != SequenceLenDesc::Fixed
    }

    /// Gets the number of index variables needed in index accessors.
    pub fn index_args(&self) -> usize {
        self.shape.dimensions() + self.is_variable_size() as usize
    }
}

/// Description of how to compute a position (of some subrange) within a buffer.
#[derive(Clone, Debug)]
pub enum FieldLocator {
    /// Static offset.
    Static(u64),

    /// Offset declared by a field.  Interpreted as relative to the start of the
    /// buffer.
    RefField(FieldName),

    /// Immediately after the end of another field, which might itself have a
    /// dynamic size.
    ///
    /// This requires that we can somehow compute the length of the other field.
    AfterField(FieldName),
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SequenceLenDesc {
    /// The sequence is of a fixed length and has some array shape.
    Fixed,

    /// The sequence is of some dynamic length determined by another field.
    ///
    /// This other field MUST be a `SequenceFieldLayout`.
    Variable(FieldName),
}
